/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package Sequential;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.LinkedList;
import java.util.Stack;

/**
 *
 * @author zwirek
 */
public class Parser {

    public static int bufferSize = 250;

    public ElementHandler eh;

    private enum Mode {

        TEXT, ENTITY, OPEN_TAG, CLOSE_TAG, START_TAG, ATTRIBUTE_LVALUE,
        ATTRIBUTE_EQUAL, ATTRIBUTE_RVALUE, QUOTE, IN_TAG, SINGLE_TAG, COMMENT, DONE,
        DOCTYPE, PRE, CDATA
    }
    long size = 0;
//    private final static int TEXT = 1,
//            ENTITY = 2,
//            OPEN_TAG = 3,
//            CLOSE_TAG = 4,
//            START_TAG = 5,
//            ATTRIBUTE_LVALUE = 6,
//            ATTRIBUTE_EQUAL = 9,
//            ATTRIBUTE_RVALUE = 10,
//            QUOTE = 7,
//            IN_TAG = 8,
//            SINGLE_TAG = 12,
//            COMMENT = 13,
//            DONE = 11,
//            DOCTYPE = 14,
//            PRE = 15,
//            CDATA = 16;
    Reader r;

    public Parser(String filename, ElementHandler eh) throws IOException {
        File f = new File(filename);
        r = new FileReader(f);
        size = f.length();
        this.eh = eh;
    }

    public Node parse() throws IOException, Exception {
        Stack<Node> v = new Stack<Node>();
        int depth = 0;
        Mode mode = Mode.PRE;
        //int c = 0;
        int quotec = '"';
        depth = 0;
        int c=0;
        StringBuffer sb = new StringBuffer();
        StringBuffer etag = new StringBuffer();
        String tagName = null;
        String lvalue = null;
        String rvalue = null;
        LinkedList<Attribute> attrs = null;
        Stack<Mode> st = new Stack<Mode>();
        int line = 1, col = 0;
        boolean eol = false;
        int start = 0;
        while (start != size) {//czytamy cały plik do bufora
            int actualBufferSize = (int) Math.min(bufferSize, size - start);
            char[] readBuffer = new char[actualBufferSize];
            start += actualBufferSize;
            r.read(readBuffer);
            for (int i = 0; i < actualBufferSize; i++) {
                c=readBuffer[i];
                if (c == '\n' && eol) {//mapujemy znaki nowej linii
                    eol = false;
                    continue;
                } else if (eol) {
                    eol = false;
                } else if (c == '\n') {
                    line++;
                    col = 0;
                } else if (c == '\r') {
                    eol = true;
                    c = '\n';
                    line++;
                    col = 0;
                } else {
                    col++;
                }

                if (mode.equals(Mode.DONE)) {//skończone parsowaniey
                    System.gc();
                    onEndParsing();
                    if(v.size()>0)
                        return v.firstElement();
                    else
                        return null;
                } else if (mode.equals(Mode.TEXT)) {//jestesmy pomiedzy znacznikami
                    if (c == '<') {
                        st.push(mode);
                        mode = Mode.START_TAG;
                        if (sb.length() > 0) {
                            v.lastElement().addText(sb.toString());
                            sb.setLength(0);
                        }
                    } else if (c == '&') {
                        st.push(mode);
                        mode = Mode.ENTITY;
                        etag.setLength(0);
                    } else {
                        sb.append((char) c);
                    }
                } else if (mode.equals(Mode.CLOSE_TAG)) {//wiemy ze czytamy tag zamykający
                    if (c == '>') {
                        mode = popMode(st);
                        tagName = sb.toString();
                        sb.setLength(0);
                        depth--;
                        if (depth == 0) {
                            mode = Mode.DONE;
                        }
                        if (v.peek().getName().equals(tagName)) {
                            if (v.size() > 1) {
                                Node n = v.pop();
                                n.setClosed(true);
                                boolean hold = onEndElement(n);
                                if(hold){
                                    v.peek().addNode(n);
                                }
                            } else {
                                Node n = v.peek();
                                n.setClosed(true);
                                boolean hold = onEndElement(n);
                                if(!hold){
                                    v.pop();
                                }
                            }
                        } else {
                            System.out.println("Weszło jakimś cudem");
                            //Rzucić exception
                        }
                    } else {
                        sb.append((char) c);
                    }
                } else if (mode.equals(Mode.CDATA)) {//czytamy CDATA
                    if (c == '>'
                            && sb.toString().endsWith("]]")) {
                        sb.setLength(sb.length() - 2);
                        v.peek().addText(sb.toString());
                        sb.setLength(0);
                        mode = popMode(st);
                    } else {
                        sb.append((char) c);
                    }
                } else if (mode.equals(Mode.COMMENT)) {//czytamy komentarz
                    if (c == '>'
                            && sb.toString().endsWith("--")) {
                        sb.setLength(0);
                        mode = popMode(st);
                    } else {
                        sb.append((char) c);
                    }
                } else if (mode.equals(Mode.PRE)) {//nie weszlismy jeszcze do zadnego elementu, nawet roota
                    if (c == '<') {
                        mode = Mode.TEXT;
                        st.push(mode);
                        mode = Mode.START_TAG;
                    }
                } else if (mode.equals(Mode.DOCTYPE)) {//DOCTYPE
                    if (c == '>') {
                        mode = popMode(st);
                        if (mode.equals(Mode.TEXT)) {
                            mode = Mode.PRE;
                        }
                    }
                } else if (mode.equals(Mode.START_TAG)) {//znaleźliśmy znak <
                    mode = popMode(st);
                    if (c == '/') {
                        st.push(mode);
                        mode = Mode.CLOSE_TAG;
                    } else if (c == '?') {
                        mode = Mode.DOCTYPE;
                    } else {
                        st.push(mode);
                        mode = Mode.OPEN_TAG;
                        tagName = null;
                        attrs = new LinkedList<Attribute>();
                        sb.append((char) c);
                    }
                } else if (mode.equals(Mode.ENTITY)) {//encje
                    if (c == ';') {
                        mode = popMode(st);
                        String cent = etag.toString();
                        etag.setLength(0);
                        if (cent.equals("lt")) {
                            sb.append('<');
                        } else if (cent.equals("gt")) {
                            sb.append('>');
                        } else if (cent.equals("amp")) {
                            sb.append('&');
                        } else if (cent.equals("quot")) {
                            sb.append('"');
                        } else if (cent.equals("apos")) {
                            sb.append('\'');
                        } // Could parse hex entities if we wanted to
                        //else if(cent.startsWith("#x"))
                        //sb.append((char)Integer.parseInt(cent.substring(2),16));
                        else if (cent.startsWith("#")) {
                            sb.append((char) Integer.parseInt(cent.substring(1)));
                        } // Insert custom entity definitions here
                        else {
                            exc("Unknown entity: &" + cent + ";", line, col);
                        }
                    } else {
                        etag.append((char) c);
                    }
                } else if (mode.equals(Mode.SINGLE_TAG)) {//pojedyncze tagi jak np <b/
                    if (tagName == null) {
                        tagName = sb.toString();
                    }
                    if (c != '>') {
                        exc("Expected > for tag: <" + tagName + "/>", line, col);
                    }
                    Node n = new Node(tagName);
                    n.setClosed(true);
                    n.addAllAtt(attrs);
                    v.peek().addNode(n);
                    if (depth == 0) {
                        System.gc();
                        boolean hold = onEndElement(n);
                        if(!hold)
                            v.pop();
                        onEndParsing();
                        if(v.size()>0)
                            return v.firstElement();
                        else
                            return null;
                    }
                    sb.setLength(0);
                    attrs = new LinkedList<Attribute>();
                    tagName = null;
                    mode = popMode(st);
                    boolean hold = onEndElement(n);
                    if(!hold){
                        v.pop();
                    }
                } else if (mode.equals(Mode.OPEN_TAG)) {//jesteśmy po przeczytaniu <
                    if (c == '>') {
                        if (tagName == null) {
                            tagName = sb.toString();
                        }
                        sb.setLength(0);
                        depth++;
                        Node n = new Node(tagName);
                        n.addAllAtt(attrs);
                        v.add(n);
                        tagName = null;
                        attrs = new LinkedList<Attribute>();
                        mode = popMode(st);
                        onStartElement(n);
                    } else if (c == '/') {
                        mode = Mode.SINGLE_TAG;
                    } else if (c == '-' && sb.toString().equals("!-")) {
                        mode = Mode.COMMENT;
                    } else if (c == '[' && sb.toString().equals("![CDATA")) {
                        mode = Mode.CDATA;
                        sb.setLength(0);
                    } else if (c == 'E' && sb.toString().equals("!DOCTYP")) {
                        sb.setLength(0);
                        mode = Mode.DOCTYPE;
                    } else if (Character.isWhitespace((char) c)) {
                        tagName = sb.toString();
                        sb.setLength(0);
                        mode = Mode.IN_TAG;
                    } else {
                        sb.append((char) c);
                    }
                } else if (mode.equals(Mode.QUOTE)) {//prawa strona atrybutu umieszczona w ciapkach
                    if (c == quotec) {
                        rvalue = sb.toString();
                        sb.setLength(0);
                        attrs.add(new Attribute(lvalue, rvalue));
                        mode = Mode.IN_TAG;
                    } else if (" \r\n\u0009".indexOf(c) >= 0) {
                        sb.append(' ');
                    } else if (c == '&') {
                        st.push(mode);
                        mode = Mode.ENTITY;
                        etag.setLength(0);
                    } else {
                        sb.append((char) c);
                    }

                } else if (mode.equals(Mode.ATTRIBUTE_RVALUE)) {//po = w atrybucie
                    if (c == '"' || c == '\'') {
                        quotec = c;
                        mode = Mode.QUOTE;
                    } else if (Character.isWhitespace((char) c)) {
                        ;
                    } else {
                        exc("Error in attribute processing", line, col);
                    }

                } else if (mode.equals(Mode.ATTRIBUTE_LVALUE)) {//lewa strona parametru
                    if (Character.isWhitespace((char) c)) {
                        lvalue = sb.toString();
                        sb.setLength(0);
                        mode = Mode.ATTRIBUTE_EQUAL;
                    } else if (c == '=') {
                        lvalue = sb.toString();
                        sb.setLength(0);
                        mode = Mode.ATTRIBUTE_RVALUE;
                    } else {
                        sb.append((char) c);
                    }

                } else if (mode.equals(Mode.ATTRIBUTE_EQUAL)) {//=
                    if (c == '=') {
                        mode = Mode.ATTRIBUTE_RVALUE;
                    } else if (Character.isWhitespace((char) c)) {
                        ;
                    } else {
                        exc("Error in attribute processing.", line, col);
                    }

                } else if (mode.equals(Mode.IN_TAG)) {//jesteśmy w środku znacznika < . >
                    if (c == '>') {
                        mode = popMode(st);
                        Node n = new Node(tagName);
                        n.addAllAtt(attrs);
                        v.add(n);
                        depth++;
                        tagName = null;
                        attrs = new LinkedList<Attribute>();
                    } else if (c == '/') {
                        mode = Mode.SINGLE_TAG;
                    } else if (Character.isWhitespace((char) c)) {
                    } else {
                        mode = Mode.ATTRIBUTE_LVALUE;
                        sb.append((char) c);
                    }
                }
            }
        }
        if (mode.equals(Mode.DONE))//przetworzone wszystko
        {
            System.gc();
            onEndParsing();
            return v.firstElement();
        } else {
            exc("missing end tag", line, col);
        }

        System.gc();
        onEndParsing();
        return v.firstElement();
    }

    private static Mode popMode(Stack<Mode> st) {
        if (!st.empty()) {
            return st.pop();
        } else {
            return Mode.PRE;
        }
    }

    private static void exc(String s, int line, int col)//wyjątek
            throws Exception {
        throw new Exception(s + " near line " + line + ", column " + col);
    }

    protected void onStartElement(Node n){
        eh.onStartElement(n);
    }

    protected boolean onEndElement(Node n){
        return eh.onEndElement(n);
    }

    protected void onEndParsing(){
        eh.onEndParsing();
    }
}
